/*******************************************************************************
 *
 *      Filename: Image.cpp
 *   Description: See Image.h
 *        Author: [kb]ypp
 *    Created at: October 14, 2005
 * Last modified: November 26, 2005
 *
 ******************************************************************************/

#include "Image.h"
#include "Log.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <memory>
#include <sstream>
#include <string>

using namespace std;

//! @brief Struct to store image data
struct Image::ImageImpl
{
	ImageImpl();
	ImageImpl( const ImageImpl& );
	ImageImpl( int w, int h, Image::Format comp );
	~ImageImpl();

	int width;                  //!< Width of stored image
	int height;                 //!< Height of stored image
	Image::Format components;   //!< Number of color components in image
	unsigned char *data;        //!< Pixel rows - bottom to top, left to right

private:
	const ImageImpl& operator=( const ImageImpl& );
};

Image::ImageImpl::ImageImpl()
	: width( 0 ), height( 0 ), components( Image::eNone ), data( 0 )
{
}

Image::ImageImpl::ImageImpl( int w, int h, Image::Format comp )
	: width( w ), height( h ), components( comp ),
	  data( new unsigned char[ width * height * components ] )
{
}

Image::ImageImpl::ImageImpl( const ImageImpl &other )
	: width( other.width ), height( other.height ),
	  components( other.components ),
	  data( new unsigned char[ width * height * components ] )
{
	const int SZ = width * height * components;
	copy( other.data, other.data + SZ, data );
}

Image::ImageImpl::~ImageImpl()
{
	width = 0;
	height = 0;
	components = eNone;
	delete [] data;
	data = 0;
}

//! @brief Default constructor
//!
//! Initialize image to be empty
Image::Image()
	: impl( new ImageImpl  )
{
}

//! @brief Copy-constructor
//!
//! Just copy other image's data
Image::Image( const Image &other )
	: impl( new Image::ImageImpl( *other.impl ) )
{
}

//! @brief Constructor
//!
//! Initialize image to be empty and try to load
//! it's data from the file `filename'
Image::Image( const string &filename )
	: impl( new ImageImpl )
{
	try {
		load( filename );
	} catch ( ... ) {
		delete impl;
		throw;
	}
}

//! @brief Virtual destructor
//!
//! Cleans up current image data
Image::~Image()
{
	delete impl;
	impl = 0;
}

void Image::load( const string &filename )
{
	assert( impl );
	const size_t pos = filename.rfind( "." );
	if ( pos != string::npos ) {
		const string ext = filename.substr( pos + 1, 3 );
		/*if ( ext == "jpg" )
			load_jpeg( filename );
		else if ( ext == "png" )
			load_png( filename );
		else*/ if ( ext == "tga" )
			load_tga( filename );
		else if ( ext == "ppm" )
			load_ppm( filename );
		else
			LOG_ERROR( filename + ": Unsupported image type" );
	} else
		LOG_ERROR( "Failed to determine image type. File `" + filename +
		           "' has no extension" );

	if ( impl->components != eNone )
		LOG_INFO( "Image file `" + filename + "' loaded successfully..." );
}

void Image::save( const string &filename )
{

	if ( !impl || impl->components == eNone )
	{
		LOG_WARNING( "There is no image data to store" );
		return;
	}

	string ext( "tga" ); // Default file extension
	const size_t pos = filename.rfind( "." );
	if ( pos != string::npos )
		LOG_WARNING( filename + ": Using defult extension `" + ext + "'" );
	else
		ext = filename.substr( pos + 1, 3 );
	/*if ( ext == "jpg" )
		save_jpeg( filename );
	else if ( ext == "png" )
		save_png( filename );
	else*/ if ( ext == "tga" )
		save_tga( filename );
	else if ( ext == "ppm" )
		save_ppm( filename );
	else
		LOG_ERROR( filename + ": File with unsupported extension" );
}

int Image::width() const
{
	assert( impl );
	return impl->width;
}

int Image::height() const
{
	assert( impl );
	return impl->height;
}

Image::Format Image::format() const
{
	assert( impl );
	return impl->components;
}

const unsigned char* Image::data() const
{
	assert( impl );
	return impl->data;
}

/*
void Image::load_jpeg( const string &filename )
{
}

void Image::save_jpeg( const string &filename )
{
}

void Image::load_png( const string &filename )
{
}

void Image::save_png( const string &filename )
{
}
*/

//! @brief Swap Red and Blue image components
static void swapRB( unsigned char *data, int size, int stride )
{
	for ( int i = 0; i < size; i += stride )
		swap( data[ i ], data[ i + 2 ] );
}

//! @brief Load an image from the Truevision TGA file
//!
//! Written under "Truevision TGA file format spcification, version 2.0".
//! Supports True-color and Black-and-white (Gray-scale) images only
//! (no ColorMap support).
void Image::load_tga( const string &filename )
{
	ifstream fin( filename.c_str(), ios::in | ios::binary );
	if ( !fin ) {
		LOG_ERROR( "Failed to open file " + filename );
		return;
	}

	struct ImageSpecification
	{
		unsigned short xOrigin;        // : 16 bits
		unsigned short yOrigin;        // : 16
		unsigned short width;          // : 16
		unsigned short height;         // : 16
		unsigned char pixelDepth;      // : 8 (common values: 8, 16, 24, and 32)
		unsigned char imageDescriptor; // : 8
	};
	struct TGAHeader
	{
		unsigned char idLength;           // : 8
		unsigned char colorMapType;       // : 8
		unsigned char imageType;          // : 8
		unsigned char colorMapSpecs[ 5 ];
		ImageSpecification imageSpecs;
	};

	TGAHeader head;
	fin.read( reinterpret_cast< char* >( &head ), 18 ); // Read TGA File Header

	if ( head.idLength != 0 ) // Image ID exists?
		fin.seekg( head.idLength, ios::cur ); // Skip Image ID
	if ( head.colorMapType != 0 ) { // ColorMap present?
		LOG_ERROR( filename + ": Unsupported TGA image type (with ColorMap)" );
		return;
	}

	int w = head.imageSpecs.width;
	int h = head.imageSpecs.height;
	Format comp = eNone;

	switch ( head.imageSpecs.pixelDepth ) {
		case 8:
			comp = eLuminance;
			break;
		case 16:
			comp = eLuminanceAlpha;
			break;
		case 24:
			comp = eRGB;
			break;
		case 32:
			comp = eRGBA;
			break;
		default:
			LOG_ERROR( filename + ": Image with unsupported pixel depth" );
			return;
	}

	int numBytes = w * h * comp;
	auto_ptr< ImageImpl > img( new ImageImpl( w, h, comp ) );
	unsigned char* &data = img->data;

	switch ( head.imageType ) {
		case 2: // Uncompressed, True-color image (BGRA order)
		case 3: // Uncompressed, Black-and-white image
			fin.read( reinterpret_cast< char* >( data ), numBytes );
			break;
		case 10: // Run-length encoded, True-color image
		case 11: // Run-length encoded, Black-and-white image
			{
				assert( comp ); // components != eNone
				unsigned char *pixelData = new unsigned char[ comp ];
				int bytes = 0; // Number of bytes "sent" to the `data'
				unsigned char packetInfo, repCount;
				while ( bytes < numBytes ) {
					// Read packet info:
					fin.read( reinterpret_cast< char* >( &packetInfo ), 1 );
					repCount = ( packetInfo & 127 ) + 1;
					if ( packetInfo & 128 ) { // Which packet type?
						// Run-Lenth Packet
						fin.read( reinterpret_cast< char* >( pixelData ), comp );
						// Store it `repCount' times:
						for ( unsigned char i = 0; i < repCount; ++i ) {
							memcpy( data + bytes, pixelData, comp );
							bytes += comp;
						}
					} else {
						// Raw Packet
						// Read & Store pixel color components `repCount' times:
						for ( unsigned char i = 0; i < repCount; ++i ) {
							fin.read( reinterpret_cast< char* >( pixelData ), comp );
							// Store it:
							memcpy( data + bytes, pixelData, comp );
							bytes += comp;
						}
					}
				}
				delete [] pixelData;
			}
			break;
		default:
			LOG_ERROR( filename + ": Unsupported TGA image type" );
			return;
	}
	// True-color images stored in .tga files as BGR(A) tuples, but
	// we need an RGB(A) ones.
	if ( comp == eRGB || comp == eRGBA )
		swapRB( data, numBytes, comp );

	delete impl;
	impl = img.release();
}

void Image::save_tga( const string &filename )
{
}

//! @brief Load an image from the PPM (Portable Pixel Map) file
//!
//! Function reads only the first image in file
void Image::load_ppm( const string &filename )
{
	ifstream fin( filename.c_str() );
	if ( !fin ) {
		LOG_ERROR( "Failed to open file " + filename );
		return;
	}

	string line;
	getline( fin, line );
	if ( line != "P6" ) {
		LOG_ERROR( filename + ": Bad signature. It's not a valid PPM image file" );
		return;
	}

	int w, h;
	while ( getline( fin, line ) && line[ 0 ] == '#' ) // Skip comments
		;
	{
		istringstream s( line );
		s >> w >> h;
	}

	while ( getline( fin, line ) && line[ 0 ] == '#' ) // Skip comments
		;
	int maxval;
	{
		istringstream s( line );
		s >> maxval;
	}
	if ( maxval != 255 ) {
		LOG_ERROR( "PPM images with Maxval != 255 not supported" );
		return;
	}

	auto_ptr< ImageImpl > img( new ImageImpl( w, h, eRGB ) );
	unsigned char* &data = img->data;
	const int rowWidth = img->width * img->components;

	ostringstream s;
	s << fin.rdbuf();
	line = s.str().substr( 0, h * rowWidth );
	if ( line.length() < static_cast< size_t >( h * rowWidth ) ) {
		LOG_ERROR( filename + ": It is not a valid .ppm file" );
		return;
	}

	for ( int row = h - 1; row >= 0; --row ) // Rows from top to bottom
		for ( int col = 0; col < rowWidth; ++col ) // From left to right
			data[ col + row * rowWidth ] =
				line[ col + ( h - row - 1 ) * rowWidth ];

	delete impl;
	impl = img.release();
}

//! @brief Save an image to the PPM (Portable Pixel Map) file
//!
//! ...
void Image::save_ppm( const string &filename )
{
	ofstream fout( filename.c_str() );
	if ( !fout ) {
		LOG_ERROR( "Failed to create/rewrite file " + filename );
		return;
	}

	assert( impl );
	fout << "P6" << endl
		 << impl->width << ' ' << impl->height << endl
		 << 255 << endl; // Maxval

	const int rowWidth = impl->components * impl->width;
	for ( int row = impl->height - 1; row >= 0; --row ) {
		unsigned char *iter = &impl->data[ row * rowWidth ];
		for ( int col = 0; col < rowWidth; ++col )
			fout << *iter++;
	}
}

/*
//! @brief Clean image data
//!
//! After "cleaning" image become empty
void Image::clean()
{
}
*/
